package soundanalyzer;

public class SoundAnalyzer {
	
	  //for calculating the Fourier transform
	  private int N;
	  
	  //the sample rate of the recording
	  private int fs;
	  
	  //the FFT object
	  private FFT fft;
	  
	  //holds the FT array
	  private float[] ft;
	  
	  //holds the audio data with a window function applied
	  private float[] audioDataTemp;
	  
	  //temporary array for FT calculation: holds the unscaled FT array
	  private float[] fftSpec;

	  //arbitrary scale for the FFT, just to make sure the numbers don't get too big
	  float ftScale;
	  
	  //an array for storing pre-calculated values of sin(i.Pi/N), for use in the window function,
	  //because the sin() function is expensive
	  private float[] sinLookup = null;
	  
	  //constructor
	  public SoundAnalyzer(int fs, int N)
	  {
		  this.fs = fs;
		  this.N = N;
		  fft = new FFT(N, fs);
		  //arbitrary scale for the FFT, just to make sure the numbers don't get too big
		  ftScale = 1.0f / ((2.0f * N) * (2.0f * N));
		  
		  audioDataTemp = new float[N];
		  ft = new float[N/2];
		  
		  buildTrigLookupTable();
	  }
	  
	  /**
	   * Create a global "lookup" array of the values of sin(i.Pi/N), for use in the window function, because the sin() function is expensive.
	   */
	  
	  private void buildTrigLookupTable()
	  {
		  sinLookup = new float[N];
		  for (int i = 0; i < N; i++) {
			  sinLookup[i] = (float) Math.sin(i * Math.PI/ N);
		  }
	  }  
	  
	  /**
	   * Get the Fourier Transform from the audio data.
	   *
	   * @param short[] audioData Audio data from the microphone
	   * @return float[] mag The Fourier Transform
	   */
	 
	  public float[] calculateFourierTransform(short[] audioData)
	  {
	  	  for (int i = 0; i < N; i++) {
	  		//apply a window function to the audio data:
	  		//multiply the k-th sample by sin(k*pi/N)
		  	audioDataTemp[i] = (float) audioData[i] * sinLookup[i];
		  }
	  	
	  	  //fft.window(1); //turn on hamming window
	  	  fft.forward(audioDataTemp);
	  	  fftSpec = fft.getSpectrum();

		  for (int i = 0; i < N/2; i++) {
			  //note: fftSpec == sqrt(real^2 + imag^2)
			  ft[i] = (fftSpec[i] * fftSpec[i]) * ftScale;
		  }
		  
		  return ft;
	  }
	  

	  // center_of_mass determines a more precise estimate for the
	  // peak at x
	  private float center_of_mass( float x )
	  {
		  int a = (int) x;
		  float u=0;
		  float v=0;
		  for( int i=a-2; i<=a+3; ++i ){
			  float h = (x-i) / 3.0f; // abs(h) <= 1
			  float w = 1.0f - h*h; // >=0
			  if (i >= ft.length)
			  {
				  continue;
			  }
			  u += ft[i] * w;
			  v += ft[i] * i * w;
		  }
		  return v/u;
	  }
	 
	  
	  private float findMaxPos() {
		  float maxVal = 0;
		  int n = 0;
		  for (int i = 1; i < ft.length; ++i) {
			  if (ft[i] * i > maxVal) {
				  maxVal = ft[i] * i;
				  n = i;
			  }
		  }
		  if( n<5 ) return 0;
		  return center_of_mass(
				  center_of_mass(
				   center_of_mass(n)));
	  }

	  private float peakArea(float f) {
		  int k = (int) f;
		  return (k - 1) * ft[k - 1] + k * ft[k] + (k + 1) * ft[k + 1] + (k + 2)
				  * ft[k + 2];
	  }

	  private float totalArea() {
		  float a = 0;
		  for (int k = 0; k < ft.length; ++k)
			  a += k * ft[k];
		  return a;
	  }

	  /**
	   * 
	   * Calculate the fundamental frequency from the global Fourier Transform object.
	   * 
	   * @return float The fundamental frequency
	   * 
	   */
	  
	  public float calculateFundamentalFrequency() {
		  final float f0 = findMaxPos();
		  if( f0 < 10 ) return 0;
		  final float total = totalArea();
		  // try to divide this frequency by all values up to 50
		  float minNoise = 1e37f;
		  int imax = 1;
		  for (int i = 1; i < f0 * 0.1; ++i) {
			  // calculate total peak area as sum of all peaks
			  float a = 0;
			  for (int k = 1; f0 / i * k + 3 < ft.length; ++k) {
				  a = a + peakArea(f0 / i * k);
			  }
			  float noise = (total - a) / (1.0f - (i * 8.0f)/f0);
			  if (noise < minNoise) {
				  minNoise = noise;
				  imax = i;
			  }
		  }
		  if( minNoise / total > 0.5 ) return 0;
		  //Log.v("maxlog", "imax="+imax);
		  return f0 / imax * fs / 2 / ft.length;
	  }

	  /**
	   * 
	   * Calculate the volume from the audioData array that is read in from the recorder.
	   * 
	   * @param short[] audioData Array of audio data read in by the recorder object
	   * @return float volume The volume
	   * 
	   */

	  public float calculateVolume(short[] audioData)
	  {
		  //calculate the volume
		  float volume;
		  long samp,sumSq,avgSq;
		  
		  sumSq=0;
		  for(int i=0; i<N; i++){
			  samp=audioData[i];
			  sumSq+=(samp*samp);
		  }
		  avgSq=sumSq/N;
		  if (avgSq > 0.0)
		  {
			  volume=(float)(10.0*Math.log10(avgSq*Math.pow(2,-29)));
		  } else
		  {
			  volume = 0.0f;
		  }
		  return volume;
	  }
	
}
